iT邦幫忙

2

ADXL345硬體I2C讀寫範例(標準庫、HAL庫)

  • 分享至 

  • xImage
  •  

這邊我記錄一下標準庫和HAL庫撰寫一樣的感測器的差別,看完就知道HAL庫多方便多快速已經很接近Arduino了。

看完這篇一定可以知道我前面幾篇文章為什麼會說想真正搞懂STM32開發的人要先學標準庫再去學HAL庫的原因了。

首先是標準庫,利用STM32F0這顆比較低階的MCU來寫三軸感測器的讀寫,這邊我會用硬體的方式來做讀寫,我前面文章有利用過軟體定義I2C的方式來做,有興趣的可以往回看看差別,有讀者還不太瞭解I2C的話可以看以下整理好的連結,這些是我之前比賽所發的文章:

[DAY 9] I²C協議原理介紹
[DAY 10] 軟體實現I2C協議
[DAY 11] 軟體實現I2C協議以三軸感測器為例 (ADXL345)
[DAY 12] 三軸感測器讀取函示講解 (ADXL345)
[DAY 13] ADXL345_I2C時序說明 (ADXL345)

我前面文章有做了不少介紹這邊就不再重複描述原理了,我會直接貼上標準庫和HAL庫的程式碼並簡單說明

STM32F0 標準庫(ADXL345)

IIC_ADXL345_Hardware.h

/*等待超時時間*/
#define 	I2CT_FLAG_TIMEOUT         ((uint32_t)0x1000)
#define 	I2CT_LONG_TIMEOUT         ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))


#define DEVICE_ID       0X00    //器件ID,0XE5
#define THRESH_TAP      0X1D    //敲擊閥值
#define OFSX            0X1E
#define OFSY            0X1F
#define OFSZ            0X20
#define DUR             0X21
#define Latent          0X22
#define Window          0X23 
#define THRESH_ACK      0X24
#define THRESH_INACT    0X25 
#define TIME_INACT      0X26
#define ACT_INACT_CTL   0X27     
#define THRESH_FF       0X28    
#define TIME_FF         0X29 
#define TAP_AXES        0X2A  
#define ACT_TAP_STATUS  0X2B 
#define BW_RATE         0X2C 
#define POWER_CTL       0X2D 
#define INT_ENABLE      0X2E
#define INT_MAP         0X2F
#define INT_SOURCE      0X30
#define DATA_FORMAT     0X31
#define DATA_X0         0X32
#define DATA_X1         0X33
#define DATA_Y0         0X34
#define DATA_Y1         0X35
#define DATA_Z0         0X36
#define DATA_Z1         0X37
#define FIFO_CTL        0X38
#define FIFO_STATUS     0X39

//0X0B TO OX1F Factory Reserved  
//如果ALT ADDRESS脚(12脚)接地,ADXL地址为0X53(不包含最低位).
//如果接V3.3,则ADXL地址为0X1D(不包含最低位).
//因为开发板接V3.3,所以转为读写地址后,为0X3B和0X3A(如果接GND,则为0XA7和0XA6)  

#define ADXL345_ADDRESS			0xA6

uint8_t ADXL345_Write_Register(uint8_t addr,uint8_t val);
uint8_t ADXL345_Read_Register(uint8_t addr);
uint8_t ADXL345_I2C_Init(void);
void ADXL345_xyz_Printf_test(void);
#endif /*__BSP_I2C_adxl_H*/

這邊直接貼上程式碼,再看看之前的利用軟體定義I2C寫ADXL345的驅動沒甚麼太大差別,重點是.c要如何使用標準庫的方式來實現?網路上的資料大多都是軟體的方式來實現I2C,本人當初也是想說少碰點壁直接用軟體定義I2C比較快也比較好懂,但懂軟體後覺得這樣不行硬體的方式也要懂,所以花了一點點時間來研究硬體的方式,在這做個紀錄。

IIC_ADXL345_Hardware.c

#include "IIC_ADXL345_Hardware.h"
#include "SysTick.h"

static __IO uint32_t  I2CTimeout = I2CT_LONG_TIMEOUT;   
static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode);

static void I2C_GPIO_Config(void)
{
 GPIO_InitTypeDef  GPIO_InitStructure;
 RCC_AHBPeriphClockCmd(ADXL345_I2C_SCL_GPIO_CLK,ENABLE);
 GPIO_InitStructure.GPIO_Pin = ADXL345_I2C_SCL_PIN | ADXL345_I2C_SDA_PIN;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_Level_3;
 GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
 GPIO_Init(ADXL345_I2C_SDA_GPIO_PORT, &GPIO_InitStructure);
 
 GPIO_PinAFConfig(ADXL345_I2C_SCL_GPIO_PORT,ADXL345_I2C_SCL_SOURCE,ADXL345_I2C_SCL_AF);
 GPIO_PinAFConfig(ADXL345_I2C_SDA_GPIO_PORT,ADXL345_I2C_SDA_SOURCE,ADXL345_I2C_SDA_AF);
}

static void I2C_Mode_Configu(void)
{
 I2C_InitTypeDef  I2C_InitStructure;

 RCC_I2CCLKConfig(RCC_I2C1CLK_SYSCLK);
 ADXL345_I2C_CLK_INIT(ADXL345_I2C_CLK, ENABLE);  /*開啟I2C的時鐘*/	
 
 /* I2C 配置 */
 I2C_InitStructure.I2C_Timing=0x10D05E82;
 I2C_InitStructure.I2C_AnalogFilter=I2C_AnalogFilter_Enable;
 I2C_InitStructure.I2C_DigitalFilter=0;
 I2C_InitStructure.I2C_Mode=I2C_Mode_I2C;
 I2C_InitStructure.I2C_OwnAddress1=0;
 I2C_InitStructure.I2C_Ack=I2C_Ack_Enable;
 I2C_InitStructure.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;
	
 I2C_Init(ADXL345_I2C, &I2C_InitStructure);
 I2C_Cmd(ADXL345_I2C,ENABLE);
 I2C_AcknowledgeConfig(ADXL345_I2C,ENABLE);
}

uint8_t ADXL345_I2C_Init(void)
{
 I2C_GPIO_Config(); 
 I2C_Mode_Configu();
 delay_ms(2);
 
 if(ADXL345_Read_Register(DEVICE_ID) == 0xE5)
 {
  ADXL345_Write_Register(DATA_FORMAT,0X2B);   //**0x31** 低電平中斷輸出,13位元全解析度,輸出資料右對齊,16g量程
  ADXL345_Write_Register(BW_RATE,0x0A);       //**0x2C** 資料輸出速度為400Hz	0A>100HZ
  ADXL345_Write_Register(POWER_CTL,0x28);     //**0X2D** 連結使能,測量模式
  ADXL345_Write_Register(INT_ENABLE,0x00);    //不使用中斷 
  
  //*************偏移寄存器**************
  ADXL345_Write_Register(OFSX,0x00);
  ADXL345_Write_Register(OFSY,0x00);
  ADXL345_Write_Register(OFSZ,0x00);
  printf("/r/nADXL345_OK");
 }
 else
 {
  printf("/r/nADXL345_NO");
 }
  return 1;  
}
 
 /* addr為ADXL345的內部暫存器地址,val為要寫入的數值 */
uint8_t ADXL345_Write_Register(uint8_t addr,uint8_t val)
{
  /*等待I2C Bus空閒*/
  I2CTimeout = I2CT_FLAG_TIMEOUT;
  while(I2C_GetFlagStatus(ADXL345_I2C,I2C_FLAG_BUSY) != RESET)
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);   
  }
  /*發送設備的地址來呼叫ADXL345,並設定為寫模式,以自動模式來發結束停止位*/  
  I2C_TransferHandling(I2C1,ADXL345_ADDRESS,2,I2C_AutoEnd_Mode ,I2C_Generate_Start_Write);
  
  /*等待I2C 發送數據完畢*/
  I2CTimeout = I2CT_FLAG_TIMEOUT;
  while(I2C_GetFlagStatus(ADXL345_I2C,I2C_FLAG_TXIS) == RESET)  
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
  }
  
  /*發送ADXL345內部暫存器地址*/
  I2C_SendData(I2C1,addr);
  
  /*等待I2C 發送數據完畢*/	
  I2CTimeout = I2CT_FLAG_TIMEOUT;
  while(I2C_GetFlagStatus(ADXL345_I2C,I2C_FLAG_TXIS) == RESET)  
  {
   if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
  }
  
  /*發送要給的數據*/
  I2C_SendData(I2C1,val);
  return 1;
}

/* 讀ADXL345寄存器,addr:寄存器地址,返回值:讀到的值 */
uint8_t ADXL345_Read_Register(uint8_t addr)
{
 uint8_t temp=0; 
  
 /*等待I2C Bus空閒*/  
 I2CTimeout = I2CT_FLAG_TIMEOUT;
 while(I2C_GetFlagStatus(ADXL345_I2C,I2C_FLAG_BUSY) != RESET)
 {
  if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5);   
 }	
 
 /*發送設備的地址來呼叫ADXL345,並設定為寫模式,以軟體方式發結束停止位*/ 
 I2C_TransferHandling(I2C1,ADXL345_ADDRESS,1,I2C_SoftEnd_Mode ,I2C_Generate_Start_Write);
	
 /*等待I2C 發送數據完畢*/  
 I2CTimeout = I2CT_FLAG_TIMEOUT;
 while(I2C_GetFlagStatus(ADXL345_I2C,I2C_FLAG_TXIS) == RESET)  
 {
  if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6);
 }
 
 /*發送ADXL345內部暫存器地址*/	
 I2C_SendData(I2C1,addr);
 
 /*等待I2C 發送數據完畢*/	
 I2CTimeout = I2CT_FLAG_TIMEOUT;
 while(I2C_GetFlagStatus(I2C1,I2C_FLAG_TC) == RESET)  
 {
  if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7);
 }	
 //*********************************************
 /* 再發送一次起始信號並設定為讀模式,自動發起結束停止位*/
 I2C_TransferHandling(I2C1,ADXL345_ADDRESS,1,I2C_AutoEnd_Mode,I2C_Generate_Start_Read);

 /*等待接收暫存器接收完畢*/
 I2CTimeout = I2CT_FLAG_TIMEOUT;
 while(I2C_GetFlagStatus(ADXL345_I2C,I2C_FLAG_RXNE) == RESET)
 {
  if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8); 
 }
  temp = I2C_ReceiveData(I2C1); /*儲存在接收器的數值*/
  return temp;                  /*回傳接到的數值*/         
} 

/**
  * @brief  Basic management of the timeout situation.
  * @param  errorCode:錯誤代碼,可以用來定位是哪個地方出錯.
  * @retval 返回0,表示IIC讀取失敗.
  */
static  uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
{
  /* Block communication and all processes */
  printf("I2C!errorCode = %d",errorCode);
  return 0;
}

void ADXL345_xyz_Printf_test(void)
{
	int xla, xha, yla, yha, zla, zha;
	float x, y, z;
	xla=ADXL345_Read_Register(0x32);// 取得 X 軸 低位元資料
	xha=ADXL345_Read_Register(0x33);// 取得 X 軸 高位元資料
	x = (((short)(xha << 8)) + xla) / 256.0;
	yla=ADXL345_Read_Register(0x34);// 取得 Y 軸 低位元資料
	yha=ADXL345_Read_Register(0x35);// 取得 Y 軸 高位元資料
	y = (((short)(yha << 8)) + yla) / 256.0;
	zla=ADXL345_Read_Register(0x36);// 取得 Z 軸 低位元資料
	zha=ADXL345_Read_Register(0x37);// 取得 Y 軸 高位元資料
    z = (((short)(zha << 8)) + zla) / 256.0;
	printf("X=%.3f Y=%.3f Z=%.3f\r\n",x,y,z);
}

main.c

int main(void)
{	
 USART_Config();			/*初始化UART*/
 ADXL345_I2C_Init(); /*初始化ADXL345*/
 delay_ms(2);
 while(1)
 {
  ADXL345_xyz_Printf_test();
  delay_ms(200);
 }
}

這樣用個UART串口就可以看到ADXL345在輸出了,在這邊大概分享一下我如何自學標準庫的,首先要來stm32f0xx_i2c.h這標準庫的最底下看看有哪些函式可以使用,這部分每顆IC都會有些不一樣,像我也有自學F429的MCU因為野火這家廠商也免費開源所有資料包含程式和書籍PDF檔,所以我在學F429在移植到F030一開始以為會差不多,F429有使用到的函示反而F030沒有,但其實都一樣只是某些功能整合在同一個函式了,但HAL庫就比較不會有這問題產生的函示幾乎都一樣就差在不同型號的功能多寡而已,只要一種MCU寫出來換另一顆只需複製貼上後小修改就好相當方便。

STM32F0 STM32CubeMX所產生的HAL庫(ADXL345)

可以看到到我上面標準庫寫的一大堆配置,接下來我會貼上我利用STM32CubeMX所產生的開發環境後所撰寫的程式,至於使用的函示會跟標準庫一樣。
STM32CubeMX的配置方法我就不描述了,網路上有很多資料很簡單不太會遇到甚麼問題

Application/User/Core底下的i2c.h

這個檔案不是我自己創建的是STM32CubeMX所產生一個檔案,我會把三軸的驅動寫在i2c.h和i2c.c裡面

/* USER CODE BEGIN Header */
/* USER CODE END Header */
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __I2C_H__
#define __I2C_H__

#ifdef __cplusplus
extern "C" {
#endif

/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* USER CODE BEGIN Includes */
#include "usart.h"
#include "math.h"

/* USER CODE END Includes */

extern I2C_HandleTypeDef hi2c1;

/* USER CODE BEGIN Private defines */
#define 	I2CT_FLAG_TIMEOUT         ((uint32_t)0x1000)
#define 	I2CT_LONG_TIMEOUT         ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))

#define DEVICE_ID       0x00    //器件ID,0XE5
#define THRESH_TAP      0x1D    //敲擊閥值
#define OFSX            0x1E
#define OFSY            0x1F
#define OFSZ            0x20
#define DUR             0x21
#define Latent          0x22
#define Window          0x23 
#define THRESH_ACK      0x24
#define THRESH_INACT    0x25 
#define TIME_INACT      0x26
#define ACT_INACT_CTL   0x27     
#define THRESH_FF       0x28    
#define TIME_FF         0x29 
#define TAP_AXES        0x2A  
#define ACT_TAP_STATUS  0x2B 
#define BW_RATE         0x2C 
#define POWER_CTL       0x2D 
#define INT_ENABLE      0x2E
#define INT_MAP         0x2F
#define INT_SOURCE      0x30
#define DATA_FORMAT     0x31
#define DATA_X0         0x32
#define DATA_X1         0x33
#define DATA_Y0         0x34
#define DATA_Y1         0x35
#define DATA_Z0         0x36
#define DATA_Z1         0x37
#define FIFO_CTL        0x38
#define FIFO_STATUS     0x39

#define ADXL345_ADDRESS			0xA6

/* USER CODE END Private defines */

void MX_I2C1_Init(void);

/* USER CODE BEGIN Prototypes */
uint8_t ADXL345_Write_Register(uint8_t addr,uint8_t val);
uint8_t ADXL345_Read_Register(uint8_t addr);
uint8_t ADXL345_I2C_Init(void);
void ADXL345_xyz_Printf_test(void);

/* USER CODE END Prototypes */

#ifdef __cplusplus
}
#endif

#endif /* __I2C_H__ */

可以看到我新增的程式碼一定都會在CODE BEGIN和CODE END中間,這樣在重新GENERATE CODE的時候才不會被刪除掉,可以對比我上的標準庫IIC_ADXL345_Hardware.h差不多在來是最重要的i2c.c

Application/User/Core底下的i2c.c

這邊我不全部貼上了直接貼上2個最重的寫和讀函式

uint8_t ADXL345_Write_Register(uint8_t addr,uint8_t val)
{
	HAL_I2C_Mem_Write(&hi2c1,ADXL345_ADDRESS, addr,1,&val,1, 1000);
	return 0;
}

uint8_t ADXL345_Read_Register(uint8_t addr)
{
	uint8_t temp=0;
	HAL_I2C_Mem_Read(&hi2c1,ADXL345_ADDRESS,addr,1,&temp,1,1000);
	return temp;
}

這兩個三軸讀寫程式跟上方標準庫的硬體I2C是做一樣的事!這已經拿邏輯分析儀驗證過了沒問題,到這可以理解位什麼前面我會說相當接近arduino了吧。

/images/emoticon/emoticon04.gif

至於這個函式去哪看?一樣也能套用標準庫的學習方式,去找官方寫好的stm32l0xx_hal_i2c.h的最底下看看宣告了哪些的函式,可以看到相當多的函式但也不太擔心,STM32的標準庫都有做詳細的註解
上面我只使用了這兩個函式

HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress,uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);

HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress,uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);

這程式要輸入的參數可以再跳進去看,這邊先以HAL_I2C_Mem_Write來做簡單說明:
https://ithelp.ithome.com.tw/upload/images/20220821/20141979I2wLlSir9s.png
上方有詳細說明每個參數要輸入什麼,看不太懂的可以直接去GOOGLE上就懂了!

I2C_HandleTypeDef hi2c式看你要指向哪個I2C

uint16_t DevAddress 是從機設備的地址
uint16_t MemAddress 是從機暫存器的地址
uint16_t MemAddSize 是要寫入暫存器地字節數,1字節是等於8bit
uint8_t pData 是要寫入暫存器的資料
uint16_t Size 這是要寫速資料的字節數,這邊也是1
uint32_t Timeout 這個是等待超時時間,就跟我標準所寫While輪巡等待時間一樣的意思

打完這幾個參數就可以完成我標準庫烙烙長的程式....

看到這裡知道先學標準庫的好處了吧,假設今天初心者直接使用HAL庫也順利打出程式,根本不會了解三軸I2C傳輸到底做了甚麼事,這個STM32CubeMX真的超方便的!!
有疑問都可以在下方提出來,我懂的話會盡力回答~
希望我這個分享對初學者碰STM32有很大的幫助!!


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言